<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>动态红色节点网络</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background: #fff;
      width: 100vw;
      height: 100vh;
    }
    canvas {
      display: block;
    }
  </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
// 获得2d图像的绘图工具
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
// 获取浏览器窗口的内部宽度
let height = window.innerHeight;
// 获取浏览器窗口的内部高度
canvas.width = width;
canvas.height = height;

const NODE_COUNT = 100; 
const NODE_RADIUS = 4; // 节点半径
const LINE_DIST = 160; // 线距离
const MOUSE_LINE_DIST = 180; // 鼠标与线的距离
const CIRCLE_RADIUS = 90; // 鼠标圆圈半径
const ATTRACT_SPEED = 0.2; // 吸引到圆圈的速度

let nodes = []; // 节点数组
let mouse = { x: width/2, y: height/2, active: false }; 
// 鼠标数组为浏览器的宽度和高度 / 2

// 生成节点
function randomNode() {
  const hue = Math.random() * 360;
  // 色调
  return {
    x: Math.random() * width,
    // 出生的x点
    y: Math.random() * height,
    // 出生的y点
    vx: (Math.random() - 0.5) * 1.2,
    vy: (Math.random() - 0.5) * 1.2,
    hue: hue,
    color: `hsla(${hue}, 80%, 60%, 0.85)`
  };
}
// 添加随机生成节点的数量
for(let i=0; i<NODE_COUNT; i++) {
  // 多次调用生成节点
  nodes.push(randomNode());
}

// 监听鼠标
canvas.addEventListener('mousemove', e => {
  mouse.x = e.clientX;
  mouse.y = e.clientY;
  mouse.active = true;
});
// 
canvas.addEventListener('mouseleave', () => {
  mouse.active = false;
});

// 动画主循环
function animate() {
  // 使用画布清楚内容
  ctx.clearRect(0, 0, width, height);
  // 节点移动
  nodes.forEach(node => {
    // 遍历每一个节点
    if(mouse.active) {
      // 检测节点距离是否小于鼠标节点
      let dx = node.x - mouse.x;
      let dy = node.y - mouse.y;
      let dist = Math.sqrt(dx*dx + dy*dy);
      // 如果节点小于鼠标半径
      if(dist < CIRCLE_RADIUS) {
        // 目标点在圆圈上
        let angle = Math.atan2(dy, dx);
        // 找到节点对于鼠标的角度 （此处直接就计算出了节点的目标地点）
        let targetX = mouse.x + Math.cos(angle) * CIRCLE_RADIUS;
        let targetY = mouse.y + Math.sin(angle) * CIRCLE_RADIUS;
        // 
        node.x += (targetX - node.x) * ATTRACT_SPEED;
        node.y += (targetY - node.y) * ATTRACT_SPEED;
      } else {
        node.x += node.vx;
        node.y += node.vy;
      }
    } else {
      node.x += node.vx;
      node.y += node.vy;
    }

    // 边界反弹
    if(node.x < NODE_RADIUS || node.x > width - NODE_RADIUS) node.vx *= -1;
    if(node.y < NODE_RADIUS || node.y > height - NODE_RADIUS) node.vy *= -1;
    node.x = Math.max(NODE_RADIUS, Math.min(width - NODE_RADIUS, node.x));
    node.y = Math.max(NODE_RADIUS, Math.min(height - NODE_RADIUS, node.y));
  });

  // 画线
  for(let i=0; i<nodes.length; i++) {
    for(let j=i+1; j<nodes.length; j++) {
      // 对于每一对点都要遍历
      let dx = nodes[i].x - nodes[j].x;
      let dy = nodes[i].y - nodes[j].y;
      let dist = Math.sqrt(dx*dx + dy*dy);
      // 当两点的距离小于line产生长度后画线
      if(dist < LINE_DIST) {
        // 保存当前canvas绘图的状态
        ctx.save();
        // 计算计算线段透明度，越接近图形的透明度越低
        const alpha = 0.18 + 0.25 * (1 - dist / LINE_DIST);
        // 线的样式
        ctx.strokeStyle = `hsla(${nodes[i].hue}, 80%, 60%, ${alpha})`;
        // 线的宽度
        ctx.lineWidth = 1.2;
        // 开始绘制一条信的绘制路径
        ctx.beginPath();
        // 定义线的起点
        ctx.moveTo(nodes[i].x, nodes[i].y);
        // 定义线的终点
        ctx.lineTo(nodes[j].x, nodes[j].y);
        // 执行绘制
        ctx.stroke();
        // 恢复到save()时保存的状态 哦~，我明白了，执行完逻辑，绘制完后再用restore，相当于提交了
        ctx.restore();
      }
    }
  }

  // 鼠标连线
  if(mouse.active) {
    nodes.forEach(node => {
      let dx = node.x - mouse.x;
      let dy = node.y - mouse.y;
      let dist = Math.sqrt(dx*dx + dy*dy);
      if(dist < MOUSE_LINE_DIST) {
        // 快存
        ctx.save();
        // 定义透明度
        const alpha = 0.22 + 0.3 * (1 - dist / MOUSE_LINE_DIST);
        // 线的样式
        ctx.strokeStyle = `hsla(${node.hue}, 80%, 60%, ${alpha})`;
        // 线的宽度
        ctx.lineWidth = 1.5;
        // 开始点
        ctx.beginPath();
        // 结束点
        ctx.moveTo(node.x, node.y);
        // 线的长度
        ctx.lineTo(mouse.x, mouse.y);
        // 执行绘制
        ctx.stroke();
        ctx.restore();
      }
    });
    // 鼠标圆圈
    ctx.save();
    ctx.strokeStyle = 'rgba(128, 128, 128, 0.25)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    // 绘制圆形
    ctx.arc(mouse.x, mouse.y, CIRCLE_RADIUS, 0, Math.PI*2);
    ctx.stroke();
    ctx.restore();
  }

  // 画节点
  nodes.forEach(node => {
    ctx.save();
    ctx.beginPath();
    ctx.arc(node.x, node.y, NODE_RADIUS, 0, Math.PI*2);
    ctx.fillStyle = node.color;
    ctx.shadowColor = 'rgba(255,0,0,0.5)';
    ctx.shadowBlur = 8;
    ctx.fill();
    ctx.restore();
  });
  // 相当于循环播放，每一帧播放一次
  requestAnimationFrame(animate);
}

// 自适应窗口
window.addEventListener('resize', () => {
  width = window.innerWidth;
  height = window.innerHeight;
  canvas.width = width;
  canvas.height = height;
});

animate();
</script>
</body>
</html> 